# coding=utf-8
# Chern-C
# Copyright (C) 2024 NJU-SAML
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import asyncio
import json
import logging
import os
import subprocess
from pathlib import Path
from typing import Iterable, List, Mapping, Optional, Union

from chernc.compiler.rust.error_messages import CargoMessage, CargoMessageTypeAdapter
from chernc.constants import DEFAULT_LOGGER_DIR, DEFAULT_LOGGER_FILE_NAME
from chernc.core.cargo import CargoConfig, Dependency, Package, Target, decode_cargo_config, read_cargo_config, write_cargo_config
from chernc.logging import build_logger

logger: logging.Logger = build_logger(__name__, logger_filename=DEFAULT_LOGGER_FILE_NAME, logger_dir=DEFAULT_LOGGER_DIR)


def locked(func):
    async def wrapper(self: "CargoManager", *args, **kwargs):
        async with self._lock:
            return await func(self, *args, **kwargs)

    return wrapper


class CargoManager:
    project_path: str

    def __init__(
        self,
        project_path: Union[str, Path],
        project_name: str = "example",
        version: str = "0.1.0",
        edition: str = "2021",
        authors: List[str] = ["Your Name <youremail@example.com>"],
        dependencies: Optional[Mapping[str, Union[str, Dependency]]] = {},
    ):
        self.project_path = str(project_path)
        self._toml_path = os.path.join(self.project_path, "Cargo.toml")
        self.config = CargoConfig(
            package=Package(name=project_name, version=version, edition=edition, authors=authors), dependencies=dependencies
        )
        self._lock = asyncio.Lock()
        self._is_closed = False

    @staticmethod
    def from_cargo_config(cargo_config: CargoConfig, project_path: Union[str, Path]) -> "CargoManager":
        project_name = "example"
        version = "0.1.0"
        authors = ["Your Name <youremail@example.com>"]
        dependencies = {}
        if cargo_config.package is not None:
            project_name = cargo_config.package.name
            version = cargo_config.package.version
            authors = cargo_config.package.authors
        if cargo_config.dependencies is not None:
            dependencies = cargo_config.dependencies
        manager = CargoManager(
            project_path=project_path,
            project_name=project_name,
            version=version,
            authors=authors,
            dependencies=dependencies,
        )
        manager.config = cargo_config
        return manager

    def __enter__(self) -> "CargoManager":
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

    def __del__(self):
        self.close()

    def close(self):
        if not self._is_closed:
            self.flush()
            self._check()
            self._is_closed = True

    def flush(self):
        if self._is_closed:
            raise Exception("Cargo manager is used after closed.")
        os.makedirs(os.path.dirname(self._toml_path), exist_ok=True)
        write_cargo_config(self.config, self._toml_path)
        # self._check()

    def reload(self):
        return read_cargo_config(os.path.join(self.project_path, "Cargo.toml"))

    def _check(self):
        # FIXME: 实践中有概率迟迟不返回
        rt = subprocess.run(
            ["cargo", "check", "--manifest-path", self._toml_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=60
        )
        if rt.stderr:
            logger.error(rt.stderr.decode())

    def build(self) -> List[CargoMessage]:
        self.flush()
        rt = subprocess.run(
            ["cargo", "build", "--tests", "--message-format=json", "--manifest-path", self._toml_path],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
        raw_messages = rt.stdout.decode().split("\n")

        messages: List[CargoMessage] = []
        for message in raw_messages:
            if message:
                messages.append(CargoMessageTypeAdapter.validate_python(json.loads(message)))
        return messages

    @locked
    async def add_dependency(self, package: str, version=""):
        """Add dependencies to a Cargo.toml manifest file

        :param package: package name
        :type package: str
        :param version: package version, defaults to ""
        :type version: str, optional
        """
        if self.config.dependencies and package in self.config.dependencies:
            return
        self.flush()
        command = f"cargo add --manifest-path {self._toml_path} {package}" + f"@{version}" if version else ""
        p = await asyncio.create_subprocess_shell(command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
        stdout, stderr = await p.communicate()
        if stderr:
            logger.error(stderr.decode())
        self.reload()

    async def add_from_config(self, config: str):
        try:
            _config = decode_cargo_config(config)
        except Exception as e:
            logger.error(e)
            return
        if _config.dependencies is None:
            return
        for package, v in _config.dependencies.items():
            if isinstance(v, str):
                version = v
            else:
                version = v.version
            await self.add_dependency(package, version)

    def dependencies(self) -> Iterable[str]:
        config = self.config
        if config.dependencies is None:
            return []
        return config.dependencies.keys()

    def add_bin(self, name: str, path: str):
        if self.config.bin is None:
            self.config.bin = []
        self.config.bin.append(Target(name=name, path=path))
        self.flush()

    def add_default_bin(self):
        if self.config.bin is None:
            self.config.bin = []
        if len(self.config.bin) == 0:
            self.config.bin.append(Target(name="main", path="src/main.rs"))
        self.flush()
